package it.webrules.deploymanager.core.st.scm.svn;

import it.webrules.deploymanager.core.OperationResult;
import it.webrules.deploymanager.core.st.scm.Info;
import it.webrules.deploymanager.core.st.scm.Log;
import it.webrules.deploymanager.core.st.scm.LogEntry;
import it.webrules.deploymanager.core.st.scm.LogEntryChangedPath;
import it.webrules.deploymanager.core.st.scm.SCMConnector;
import it.webrules.deploymanager.core.st.scm.SCMException;
import it.webrules.deploymanager.core.st.scm.Status;
import it.webrules.deploymanager.core.st.scm.SwitchableDir;
import it.webrules.deploymanager.core.st.scm.SwitchableDirs;
import it.webrules.deploymanager.core.st.scm.Type;
import it.webrules.deploymanager.core.utils.FileUtils;
import it.webrules.deploymanager.core.utils.SCMTagUtils;
import it.webrules.xsd.dmc.config.v1.StaticConfig;
import it.webrules.xsd.dmc.config.v1.StaticConfig.Scm;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import javax.xml.stream.XMLStreamException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tmatesoft.svn.core.ISVNLogEntryHandler;
import org.tmatesoft.svn.core.SVNCommitInfo;
import org.tmatesoft.svn.core.SVNDepth;
import org.tmatesoft.svn.core.SVNDirEntry;
import org.tmatesoft.svn.core.SVNException;
import org.tmatesoft.svn.core.SVNLogEntry;
import org.tmatesoft.svn.core.SVNLogEntryPath;
import org.tmatesoft.svn.core.SVNNodeKind;
import org.tmatesoft.svn.core.SVNProperties;
import org.tmatesoft.svn.core.SVNURL;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.internal.io.dav.DAVRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.fs.FSRepositoryFactory;
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
import org.tmatesoft.svn.core.internal.wc.DefaultSVNOptions;
import org.tmatesoft.svn.core.io.SVNRepository;
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
import org.tmatesoft.svn.core.wc.SVNClientManager;
import org.tmatesoft.svn.core.wc.SVNCommitClient;
import org.tmatesoft.svn.core.wc.SVNConflictChoice;
import org.tmatesoft.svn.core.wc.SVNCopyClient;
import org.tmatesoft.svn.core.wc.SVNCopySource;
import org.tmatesoft.svn.core.wc.SVNDiffClient;
import org.tmatesoft.svn.core.wc.SVNInfo;
import org.tmatesoft.svn.core.wc.SVNLogClient;
import org.tmatesoft.svn.core.wc.SVNRevision;
import org.tmatesoft.svn.core.wc.SVNStatusClient;
import org.tmatesoft.svn.core.wc.SVNUpdateClient;
import org.tmatesoft.svn.core.wc.SVNWCClient;
import org.tmatesoft.svn.core.wc.SVNWCUtil;

/**
 * SCM connector for SVN.
 * 
 * The repository must have the stardard layout:
 * http://svnbook.red-bean.com/en/1.6/svn.branchmerge.maint.html#svn.branchmerge.maint.layout
 * 
 * 
 * @author dometec
 * 
 */
public class SvnConnector implements SCMConnector {

	private final class LogEntryHandler implements ISVNLogEntryHandler {
		private final List<LogEntry> entries;

		private LogEntryHandler(List<LogEntry> entries) {
			this.entries = entries;
		}

		@Override
		@SuppressWarnings({ "unchecked", "rawtypes" })
		public void handleLogEntry(SVNLogEntry logEntry) throws SVNException {

			LogEntry entry = new LogEntry();

			entry.author = logEntry.getAuthor();
			entry.date = logEntry.getDate();
			entry.message = logEntry.getMessage();
			entry.revision = logEntry.getRevision();

			logger.trace("Getting info for revision " + entry.revision);

			SVNProperties rp = logEntry.getRevisionProperties();
			if (rp != null)
				entry.revisionProperties = rp.asMap();

			if (logEntry.getChangedPaths().size() > 0) {
				entry.changedPath = new ArrayList<LogEntryChangedPath>();
				Set changedPathsSet = logEntry.getChangedPaths().keySet();

				for (Iterator changedPaths = changedPathsSet.iterator(); changedPaths.hasNext();) {
					SVNLogEntryPath entryPath = (SVNLogEntryPath) logEntry.getChangedPaths().get(changedPaths.next());

					LogEntryChangedPath logEntryChangedPath = new LogEntryChangedPath();
					logEntryChangedPath.type = entryPath.getType();
					logEntryChangedPath.path = entryPath.getPath();
					logEntryChangedPath.fromcopypath = entryPath.getCopyPath();
					logEntryChangedPath.fromrevision = entryPath.getCopyRevision();
					entry.changedPath.add(logEntryChangedPath);
				}
			}

			entries.add(entry);

		}
	}

	private final Logger logger = LoggerFactory.getLogger(this.getClass());

	private final ISVNAuthenticationManager authManager;
	private final SVNURL projectURL;
	private final SVNClientManager clientManager;
	private final String name;
	private final String path;

	/**
	 * Create a connection to svn repository.
	 * Setup SVNKit for DAV, SVN, and FS repositories.
	 * 
	 * @throws SvnException
	 */
	public SvnConnector(StaticConfig staticConfig) throws SvnException {

		Scm scm = staticConfig.getScm();

		this.name = staticConfig.getName();
		this.path = staticConfig.getPath();

		String url = scm.getValue();
		String username = scm.getUsername();
		String password = scm.getPassword();

		DAVRepositoryFactory.setup();
		SVNRepositoryFactoryImpl.setup();
		FSRepositoryFactory.setup();

		try {

			DefaultSVNOptions options = SVNWCUtil.createDefaultOptions(true);

			this.projectURL = SVNURL.parseURIEncoded(url);
			this.authManager = SVNWCUtil.createDefaultAuthenticationManager(username, password);
			this.clientManager = SVNClientManager.newInstance(options, authManager);

		} catch (SVNException e) {
			throw new SvnException("Svn url is malformed", e);

		}
	}

	/**
	 * List all tags and branches of this project. Add trunk too if it's present.  
	 * 
	 * @return all directory that the prject can switch to.
	 * 
	 * @throws SvnException
	 */
	public SwitchableDirs getSwitchableDirs() throws SvnException {

		try {

			SwitchableDirs switchableDirs = new SwitchableDirs();

			SVNRepository repository = SVNRepositoryFactory.create(projectURL);
			repository.setAuthenticationManager(authManager);

			SVNNodeKind trunkNode = repository.checkPath("trunk", -1);
			if (trunkNode.equals(SVNNodeKind.DIR)) {
				SVNDirEntry dirEntry = repository.info("trunk", -1);
				switchableDirs.addSwitchableDir("", dirEntry.getURL().toString(), dirEntry.getCommitMessage(), dirEntry.getDate(),
						dirEntry.getRevision(), Type.Trunk);
			}

			processDir("tags", Type.Tag, switchableDirs, repository);
			processDir("branches", Type.Branch, switchableDirs, repository);

			return switchableDirs;

		} catch (Exception e) {
			throw new SvnException("Error listing tags/branches.", e);
		}
	}

	@Override
	public String getNextTagVersion() throws SCMException {
		List<SwitchableDir> switchableDirs = getSwitchableDirs().getSwitchableDirs();
		return SCMTagUtils.getNextTagVersion(switchableDirs);
	}

	private void processDir(String dir, Type type, SwitchableDirs switchableDirs, SVNRepository repository) {

		try {

			Collection<SVNDirEntry> entries = new ArrayList<SVNDirEntry>();
			repository.getDir(dir, -1, true, entries);
			for (SVNDirEntry entry : entries) {
				if (entry.getKind().equals(SVNNodeKind.DIR)) {
					switchableDirs.addSwitchableDir(entry.getName(), entry.getURL().toString(), entry.getCommitMessage(), entry.getDate(),
							entry.getRevision(), type);
				}
			}

		} catch (SVNException e) {
			// dir don't exist...
			if (!e.getMessage().contains("404"))
				logger.warn("Error listing tags/branches.", e);
		}

	}

	/**
	 * Do checkout of
	 * 
	 * @param type Identify the type of source checkout dir.
	 * @param name If type is Branch or Tag, this it's name.
	 * 
	 * @param coDir dir where perform checkout
	 */
	public OperationResult checkout(Type type, String name, Boolean forcecheckout) throws SvnException {

		try {

			boolean dorevert = false;
			File coDir = new File(path);

			if (coDir.list().length > 0) {
				if (forcecheckout == null || !forcecheckout) {
					return new OperationResult(false, "Checkout directory is not empty. Use --forcecheckout to proceed, the "
							+ "file that are present both on folder and SVN will be OVERWRITE on folder. BACKUP YOUR FILE BEFORE!");
				} else {
					dorevert = true;
					removeSVNMetadata(coDir);
				}
			}

			SVNURL coUrl = null;
			if (type.equals(Type.Branch))
				coUrl = this.projectURL.appendPath("branches", true).appendPath(name, true);
			else if (type.equals(Type.Tag))
				coUrl = this.projectURL.appendPath("tags", true).appendPath(name, true);
			else
				coUrl = this.projectURL.appendPath("trunk", true);

			SVNUpdateClient updateClient = this.clientManager.getUpdateClient();
			Long rev = updateClient.doCheckout(coUrl, coDir, SVNRevision.UNDEFINED, SVNRevision.HEAD, SVNDepth.INFINITY, true);

			if (dorevert) {
				SVNWCClient wcClient = this.clientManager.getWCClient();
				wcClient.doResolve(coDir, SVNDepth.INFINITY, SVNConflictChoice.MINE_FULL);
				wcClient.doRevert(new File[] { coDir }, SVNDepth.INFINITY, null);
			}

			return new OperationResult("Checkout at revision " + rev + ".");

		} catch (Exception e) {
			throw new SvnException("Error perform checkout.", e);
		}

	}

	public static void removeSVNMetadata(File dir) {

		if (dir.exists()) {

			File[] files = dir.listFiles();
			for (File file : files) {

				if (file.isDirectory()) {
					if (file.getName().equals(".svn"))
						FileUtils.deleteDirectory(file);
					else
						removeSVNMetadata(file);
				}

			}

		}

	}

	/**
	 * Return some information about current working copy 
	 * 
	 * @return information dettails
	 * 
	 */
	public Info info() throws SvnException {

		File coDir = new File(this.path);

		try {

			SVNWCClient wcClient = this.clientManager.getWCClient();
			SVNInfo svninfo = wcClient.doInfo(coDir, SVNRevision.HEAD);

			Info info = new Info();
			info.path = coDir.getAbsolutePath();
			info.lastauthor = svninfo.getAuthor();
			info.lastchange = svninfo.getCommittedDate();
			info.repo = svninfo.getRepositoryRootURL().toString();
			info.repourl = svninfo.getURL().toString();
			info.revision = svninfo.getRevision().getNumber();

			String relPath = svninfo.getURL().toString().replace(this.projectURL.toString(), "");
			if (relPath.contains(Type.Trunk.getDirName()))
				info.type = Type.Trunk;
			else {
				if (relPath.contains(Type.Branch.getDirName())) {
					info.type = Type.Branch;
				} else if (relPath.contains(Type.Tag.getDirName())) {
					info.type = Type.Tag;
				} else {
					throw new SvnException("Error getting information. Maybe the project doesn't have standard layout?");
				}

				info.name = relPath.substring(relPath.lastIndexOf("/") + 1);
			}

			return info;

		} catch (SVNException e) {

			if (e.getMessage().toLowerCase(Locale.ENGLISH).indexOf("is not a working copy") != -1)
				throw new SvnException("No working copy at " + coDir.getAbsolutePath());
			else
				throw new SvnException("Error getting information.", e);

		} catch (Exception e) {
			throw new SvnException("Error perform info: " + e.getMessage(), e);
		}

	}

	/**
	 * Return the update status of current working copy respect to SCM.
	 * Generally, there aren't file that differs from repository, this working copy should 
	 * by read only for any other application out of DeployManaver/SVN.
	 * 
	 * @return
	 * @throws SvnException
	 */
	public Status status() throws SvnException {

		try {

			Status status = new Status();

			File coDir = new File(this.path);
			SVNStatusClient statusClient = clientManager.getStatusClient();
			SvnStatusHandler statusHandler = new SvnStatusHandler(true, status);
			status.remoteRevision = statusClient.doStatus(coDir, SVNRevision.HEAD, SVNDepth.INFINITY, true, false, true, true,
					statusHandler, null);

			return status;

		} catch (Exception e) {
			throw new SvnException("Error perform status: " + e.getMessage(), e);
		}

	}

	@Override
	public OperationResult cleanup() throws SCMException {

		try {

			File coDir = new File(this.path);
			SVNWCClient wcClient = clientManager.getWCClient();
			wcClient.doCleanup(coDir);

			return new OperationResult();

		} catch (Exception e) {
			throw new SvnException("Error perform cleanup: " + e.getMessage(), e);
		}

	}

	/**
	 * As status, but with content of file
	 * 
	 */
	@Override
	public Object diff() throws SCMException {

		try {

			ByteArrayOutputStream baos = new ByteArrayOutputStream();

			File coDir = new File(path);
			SVNDiffClient diffClient = clientManager.getDiffClient();
			diffClient.doDiff(coDir, SVNRevision.UNDEFINED, SVNRevision.WORKING, SVNRevision.HEAD, SVNDepth.INFINITY, true, baos, null);

			return new OperationResult(new String(baos.toByteArray()));

		} catch (Exception e) {
			throw new SvnException("Error perform diff: " + e.getMessage(), e);
		}

	}

	/**
	 * 
	 * Revert any change in working copy with last source from repository.
	 * 
	 */
	@Override
	public OperationResult revert() throws SCMException {

		try {

			File coDir = new File(path);
			SVNWCClient wcClient = clientManager.getWCClient();
			wcClient.doRevert(new File[] { coDir }, SVNDepth.INFINITY, null);

			return new OperationResult();

		} catch (Exception e) {
			throw new SvnException("Error perform revert: " + e.getMessage(), e);
		}

	}

	/**
	 * Update working copy from repository.
	 * 
	 */
	@Override
	public OperationResult update() throws SCMException {

		try {

			File coDir = new File(path);
			SVNUpdateClient updateClient = clientManager.getUpdateClient();
			long rev = updateClient.doUpdate(coDir, SVNRevision.HEAD, SVNDepth.INFINITY, false, false);

			return new OperationResult("Update to revision " + rev + ".");

		} catch (Exception e) {
			throw new SvnException("Error perform update: " + e.getMessage(), e);
		}

	}

	/**
	 * Remote Copy (Tagging or Branching)  
	 * 
	 */
	@Override
	public OperationResult copy(Type srctype, String srcname, Type dsttype, String dstname) throws SCMException {

		try {

			SVNWCClient wcClient = this.clientManager.getWCClient();

			SVNURL dstUrl = projectURL.appendPath(dsttype.getDirName(), true).appendPath(dstname, true);
			SVNURL srcUrl = projectURL.appendPath(srctype.getDirName(), true);
			if (srcname != null)
				srcUrl = srcUrl.appendPath(srcname, true);

			SVNInfo svninfo = wcClient.doInfo(srcUrl, SVNRevision.HEAD, SVNRevision.HEAD);
			String commitMessage = "Copy from " + srctype;
			if (srcname != null)
				commitMessage += "/" + srcname;
			commitMessage += ", revision " + svninfo.getRevision().getNumber() + ". By DeployManager.";

			SVNCopySource[] sources = new SVNCopySource[] { new SVNCopySource(SVNRevision.HEAD, SVNRevision.HEAD, srcUrl) };

			SVNCopyClient copyClient = clientManager.getCopyClient();
			SVNCommitInfo svnCommitInfo = copyClient.doCopy(sources, dstUrl, false, true, true, commitMessage, null);

			long newRevision = svnCommitInfo.getNewRevision();
			// SVNPropertyValue propertyValue =
			// SVNPropertyValue.create("FromRevision",
			// String.valueOf(newRevision).getBytes());

			// wcClient.doSetProperty(dstUrl, srctype.toString() +
			// "FromRevision", propertyValue, SVNRevision.create(newRevision),
			// commitMessage, null, true, null);

			return new OperationResult("Copy " + srcUrl.toString() + " to " + dstUrl.toString() + " successfully, revision " + newRevision
					+ ".");

		} catch (Exception e) {
			throw new SvnException("Error perform copy: " + e.getMessage(), e);
		}

	}

	@Override
	public OperationResult switchTo(Type type, String name) throws SCMException {

		try {

			File coDir = new File(path);

			Status status = new Status();
			SVNStatusClient statusClient = clientManager.getStatusClient();
			SvnStatusHandler statusHandler = new SvnStatusHandler(true, status);
			status.remoteRevision = statusClient.doStatus(coDir, SVNRevision.HEAD, SVNDepth.INFINITY, false, false, false, true,
					statusHandler, null);
			if (status.entries.size() > 0) {
				return new OperationResult(false, "Switch are not allowed if there are local pending changes, "
						+ "check changes and do revert before (first: " + status.entries.get(0).path + ").");
			}

			SVNURL switchToUrl = projectURL;
			if (type.equals(Type.Tag))
				switchToUrl = switchToUrl.appendPath("tags", true).appendPath(name, true);
			else if (type.equals(Type.Branch))
				switchToUrl = switchToUrl.appendPath("branches", true).appendPath(name, true);
			else
				switchToUrl = switchToUrl.appendPath("trunk", true);

			SVNUpdateClient updateClient = clientManager.getUpdateClient();
			long doSwitch = updateClient.doSwitch(coDir, switchToUrl, SVNRevision.HEAD, SVNRevision.HEAD, SVNDepth.INFINITY, true, true);

			return new OperationResult("Switch to " + switchToUrl.toString() + " successfully, revision " + doSwitch + ".");

		} catch (Exception e) {
			throw new SvnException("Error perform delete: " + e.getMessage(), e);
		}

	}

	@Override
	public OperationResult delete(Type type, String name) throws SCMException {

		if (!type.equals(Type.Branch))
			return new OperationResult(false, "Can't delete trunk or tags.");

		try {

			File coDir = new File(path);
			SVNWCClient wcClient = this.clientManager.getWCClient();
			SVNInfo svninfo = wcClient.doInfo(coDir, SVNRevision.HEAD);

			String endCurrentRepo = "branches/" + name;
			if (svninfo.getURL().toString().endsWith(endCurrentRepo))
				return new OperationResult(false,
						"Can't delete origin of current working copy, do switch to other Trunk/Tag/Branch before.");

			SVNURL toDelete = projectURL.appendPath(endCurrentRepo, true);

			SVNCommitClient commitClient = clientManager.getCommitClient();
			SVNCommitInfo svnCommitInfo = commitClient.doDelete(new SVNURL[] { toDelete }, "Deleted from DeployManager", null);

			return new OperationResult("Delete " + toDelete.toString() + " successfully, revision " + svnCommitInfo.getNewRevision() + ".");

		} catch (Exception e) {
			throw new SvnException("Error perform delete: " + e.getMessage(), e);
		}

	}

	@Override
	public Log log() throws SvnException {

		final List<LogEntry> entries = new ArrayList<LogEntry>();

		try {

			SVNLogClient logClient = this.clientManager.getLogClient();
			logClient.doLog(projectURL, new String[] { "." }, SVNRevision.create(1), SVNRevision.create(1), SVNRevision.HEAD, false, true,
					false, 0, null, new LogEntryHandler(entries));

			Log log = new Log();
			log.entries = entries;
			return log;

		} catch (Exception e) {
			throw new SvnException("Error perform log: " + e.getMessage(), e);
		}

	}

	@Override
	public OperationResult createSimpleGraph(File outsvgfile) throws SvnException {

		// try {

		SVNLogClient logClient = this.clientManager.getLogClient();
		// Svnlog2SimpleSvg svnlog2SimpleSvg = new Svnlog2SimpleSvg("Graph for "
		// + name, log, new FileOutputStream(outsvgfile));
		// svnlog2SimpleSvg.generateGraph();

		// } catch (FileNotFoundException e) {
		// throw new SvnException("Error perform graph: " + e.getMessage(), e);
		// } catch (XMLStreamException e) {
		// throw new SvnException("Error perform graph: " + e.getMessage(), e);
		// } catch (IOException e) {
		// throw new SvnException("Error perform graph: " + e.getMessage(), e);

		return new OperationResult("Created " + outsvgfile.getAbsolutePath() + " successfully.");

	}

	@Override
	public OperationResult createCompleteGraph(File outsvgfile) throws SvnException {

		Log log = log();

		FileOutputStream fos = null;
		ObjectOutputStream out = null;
		try {
			fos = new FileOutputStream("/tmp/loginapp.svn");
			out = new ObjectOutputStream(fos);
			out.writeObject(log);
			out.close();
		} catch (IOException e) {
			throw new SvnException("Error perform graph: " + e.getMessage(), e);
		}

		try {

			Svnlog2Svg svnlog2SimpleSvg = new Svnlog2Svg("Graph for " + name, log, new FileOutputStream(outsvgfile));
			svnlog2SimpleSvg.generateGraph();

		} catch (FileNotFoundException e) {
			throw new SvnException("Error perform graph: " + e.getMessage(), e);
		} catch (XMLStreamException e) {
			throw new SvnException("Error perform graph: " + e.getMessage(), e);
		} catch (IOException e) {
			throw new SvnException("Error perform graph: " + e.getMessage(), e);
		}

		return new OperationResult("Created " + outsvgfile.getAbsolutePath() + " successfully.");

	}

}
